﻿// -----------------------------------------------------------------------
// <copyright company="www.3WayWebdesign.de">
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
// 
// <author>Michael Morbach</author>
// <email>Michael@3waywebdesign.de</email>
// <web>http://www.3waywebdesign.de</web>
// ----------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Windows;
using Tww.Wpf.Controls.Controls.Shared;

namespace Tww.Wpf.Controls
{
    /// <summary>
    ///     A class that allows for the detection and alteration of a MetroWindow's theme and accent.
    /// </summary>
    public static class ThemeManager
    {
        #region  - Felder (privat) -

        private static IList<Accent> accents;
        private static IList<AppTheme> appThemes;

        #endregion

        #region  - Indexer und Eigenschaften (öffentlich) -

        /// <summary>
        ///     Gets a list of all of default themes.
        /// </summary>
        public static IEnumerable<Accent> Accents
        {
            get
            {
                if (accents != null)
                    return accents;

                var colors = new[]
                {
                    "Lanora"
                };

                accents = new List<Accent>(colors.Length);

                foreach (string color in colors)
                {
                    accents.Add(new Accent(color,
                        new Uri(
                            string.Format("pack://application:,,,/Tww.Wpf.Controls;component/Styles/Accents/{0}.xaml",
                                color))));
                }

                return accents;
            }
        }

        /// <summary>
        ///     Gets a list of all of default metro themes.
        /// </summary>
        public static IEnumerable<AppTheme> AppThemes
        {
            get
            {
                if (appThemes != null)
                {
                    return appThemes;
                }

                appThemes = new List<AppTheme>();

                return appThemes;
            }
        }

        #endregion

        #region  - Methoden (öffentlich) -

        /// <summary>
        ///     Adds an accent with the given name.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="resourceAddress"></param>
        /// <returns>true if the accent does not exists and can be added.</returns>
        public static bool AddAccent(string name, Uri resourceAddress)
        {
            if (name == null) throw new ArgumentNullException("name");
            if (resourceAddress == null) throw new ArgumentNullException("resourceAddress");

            bool accentExists = GetAccent(name) != null;
            if (accentExists)
            {
                return false;
            }

            accents.Add(new Accent(name, resourceAddress));
            return true;
        }

        /// <summary>
        ///     Adds an app theme with the given name.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="resourceAddress"></param>
        /// <returns>true if the app theme does not exists and can be added.</returns>
        public static bool AddAppTheme(string name, Uri resourceAddress)
        {
            bool appThemeExists = GetAppTheme(name) != null;
            if (appThemeExists)
            {
                return false;
            }

            appThemes.Add(new AppTheme(name, resourceAddress));
            return true;
        }

        /// <summary>
        ///     Gets app theme with the given resource dictionary.
        /// </summary>
        /// <param name="resources"></param>
        /// <returns>AppTheme</returns>
        public static AppTheme GetAppTheme(ResourceDictionary resources)
        {
            return AppThemes.FirstOrDefault(x => x.Resources.Source == resources.Source);
        }

        /// <summary>
        ///     Gets app theme with the given name and theme type (light or dark).
        /// </summary>
        /// <param name="appThemeName"></param>
        /// <returns>AppTheme</returns>
        public static AppTheme GetAppTheme(string appThemeName)
        {
            return
                AppThemes.FirstOrDefault(x => x.Name.Equals(appThemeName, StringComparison.InvariantCultureIgnoreCase));
        }

        /// <summary>
        ///     Gets the inverse <see cref="AppTheme" /> of the given <see cref="AppTheme" />.
        ///     This method relies on the "Dark" or "Light" affix to be present.
        /// </summary>
        /// <param name="appTheme">The app theme.</param>
        /// <returns>The inverse <see cref="AppTheme" /> or <c>null</c> if it couldn't be found.</returns>
        /// <remarks>
        ///     Returns BaseLight, if BaseDark is given or vice versa.
        ///     Custom Themes must end with "Dark" or "Light" for this to work, for example "CustomDark" and "CustomLight".
        /// </remarks>
        public static AppTheme GetInverseAppTheme(AppTheme appTheme)
        {
            if (appTheme.Name.EndsWith("dark", StringComparison.InvariantCultureIgnoreCase))
            {
                return GetAppTheme(appTheme.Name.ToLower().Replace("dark", String.Empty) + "light");
            }

            if (appTheme.Name.EndsWith("light", StringComparison.InvariantCultureIgnoreCase))
            {
                return GetAppTheme(appTheme.Name.ToLower().Replace("light", String.Empty) + "dark");
            }

            return null;
        }

        /// <summary>
        ///     Gets the <see cref="Accent" /> with the given name.
        /// </summary>
        /// <param name="accentName"></param>
        /// <returns>The <see cref="Accent" /> or <c>null</c>, if the app theme wasn't found</returns>
        public static Accent GetAccent(string accentName)
        {
            return Accents.FirstOrDefault(x => x.Name == accentName);
        }

        /// <summary>
        ///     Gets the <see cref="Accent" /> with the given resource dictionary.
        /// </summary>
        /// <param name="resources"></param>
        /// <returns>The <see cref="Accent" /> or <c>null</c>, if the accent wasn't found.</returns>
        public static Accent GetAccent(ResourceDictionary resources)
        {
            return Accents.FirstOrDefault(x => x.Resources.Source == resources.Source);
        }

        /// <summary>
        ///     Gets a resource from the detected AppStyle.
        /// </summary>
        /// <param name="window">The window to check. If this is null, the Application's sources will be checked.</param>
        /// <param name="key">The key to check against.</param>
        /// <returns>The resource object or null, if the resource wasn't found.</returns>
        public static object GetResourceFromAppStyle(Window window, string key)
        {
            Tuple<AppTheme, Accent> appStyle = window != null
                ? DetectAppStyle(window)
                : DetectAppStyle(Application.Current);
            if (appStyle == null && window != null)
            {
                appStyle = DetectAppStyle(Application.Current); //no resources in the window's resources.
            }

            if (appStyle == null)
            {
                // nothing to do here, we can't found an app style (make sure all custom themes are added!)
                return null;
            }

            object resource = appStyle.Item1.Resources[key]; //check the theme first

            //next check the accent
            object accentResource = appStyle.Item2.Resources[key];
            if (accentResource != null)
                return accentResource;

            return resource;
        }

        /// <summary>
        ///     Change the theme for the whole application.
        /// </summary>
        /// <param name="app"></param>
        /// <param name="themeName"></param>
        [SecurityCritical]
        public static void ChangeAppTheme(Application app, string themeName)
        {
            AppTheme matched = GetAppTheme(themeName);

            if (matched != null)
            {
                Tuple<AppTheme, Accent> oldTheme = DetectAppStyle(app);
                ChangeAppStyle(app.Resources, oldTheme, oldTheme.Item2, matched);
            }
        }

        /// <summary>
        ///     Change theme for the given window.
        /// </summary>
        /// <param name="window"></param>
        /// <param name="themeName"></param>
        [SecurityCritical]
        public static void ChangeAppTheme(Window window, string themeName)
        {
            AppTheme matched = GetAppTheme(themeName);

            if (matched != null)
            {
                Tuple<AppTheme, Accent> oldTheme = DetectAppStyle(window);
                ChangeAppStyle(window.Resources, oldTheme, oldTheme.Item2, matched);
            }
        }

        /// <summary>
        ///     Change accent and theme for the whole application.
        /// </summary>
        /// <param name="app">The instance of Application to change.</param>
        /// <param name="newAccent">The accent to apply.</param>
        /// <param name="newTheme">The theme to apply.</param>
        [SecurityCritical]
        public static void ChangeAppStyle(Application app, Accent newAccent, AppTheme newTheme)
        {
            Tuple<AppTheme, Accent> oldTheme = DetectAppStyle(app);
            ChangeAppStyle(app.Resources, oldTheme, newAccent, newTheme);
        }

        /// <summary>
        ///     Change accent and theme for the given window.
        /// </summary>
        /// <param name="window">The Window to change.</param>
        /// <param name="newAccent">The accent to apply.</param>
        /// <param name="newTheme">The theme to apply.</param>
        [SecurityCritical]
        public static void ChangeAppStyle(Window window, Accent newAccent, AppTheme newTheme)
        {
            Tuple<AppTheme, Accent> oldTheme = DetectAppStyle(window);
            ChangeAppStyle(window.Resources, oldTheme, newAccent, newTheme);
        }

        /// <summary>
        ///     Changes the accent and theme of a ResourceDictionary directly.
        /// </summary>
        /// <param name="resources">The ResourceDictionary to modify.</param>
        /// <param name="newAccent">The accent to apply to the ResourceDictionary.</param>
        /// <param name="newTheme">The theme to apply to the ResourceDictionary.</param>
        [SecurityCritical]
        public static void ChangeAppStyle(ResourceDictionary resources, Accent newAccent, AppTheme newTheme)
        {
            ApplyResourceDictionary(newAccent.Resources, resources);
            ApplyResourceDictionary(newTheme.Resources, resources);
        }

        /// <summary>
        ///     Scans the window resources and returns it's accent and theme.
        /// </summary>
        public static Tuple<AppTheme, Accent> DetectAppStyle()
        {
            try
            {
                return DetectAppStyle(Application.Current.MainWindow);
            }
            catch (Exception)
            {
                return DetectAppStyle(Application.Current);
            }
        }

        /// <summary>
        ///     Scans the window resources and returns it's accent and theme.
        /// </summary>
        /// <param name="window">The Window to scan.</param>
        public static Tuple<AppTheme, Accent> DetectAppStyle(Window window)
        {
            return DetectAppStyle(window.Resources) ?? DetectAppStyle(Application.Current.Resources);
        }

        /// <summary>
        ///     Scans the application resources and returns it's accent and theme.
        /// </summary>
        /// <param name="app">The Application instance to scan.</param>
        public static Tuple<AppTheme, Accent> DetectAppStyle(Application app)
        {
            return DetectAppStyle(app.Resources);
        }

        #endregion

        #region  - Methoden (privat) -

        internal static bool DetectThemeFromAppResources(out AppTheme detectedTheme)
        {
            detectedTheme = null;

            return TryDetectThemeFromResources(ref detectedTheme, Application.Current.Resources);
        }

        internal static bool TryGetThemeFromResources(AppTheme presetTheme, ResourceDictionary dict,
            ref Tuple<AppTheme, Accent> detectedAccentTheme)
        {
            Accent matched;

            if ((matched = GetAccent(dict)) != null)
            {
                detectedAccentTheme = Tuple.Create(presetTheme, matched);
                return true;
            }

            foreach (ResourceDictionary rd in dict.MergedDictionaries)
            {
                if (TryGetThemeFromResources(presetTheme, rd, ref detectedAccentTheme))
                {
                    return true;
                }
            }

            return false;
        }

        [SecurityCritical]
        private static void ChangeAppStyle(ResourceDictionary resources, Tuple<AppTheme, Accent> oldThemeInfo,
            Accent newAccent, AppTheme newTheme)
        {
            bool themeChanged = false;
            if (oldThemeInfo != null)
            {
                Accent oldAccent = oldThemeInfo.Item2;
                if (oldAccent != null && oldAccent.Name != newAccent.Name)
                {
                    ResourceDictionary oldAccentResource =
                        resources.MergedDictionaries.FirstOrDefault(d => d.Source == oldAccent.Resources.Source);
                    if (oldAccentResource != null)
                    {
                        resources.MergedDictionaries.Add(newAccent.Resources);
                        resources.MergedDictionaries.Remove(oldAccentResource);

                        themeChanged = true;
                    }
                }

                AppTheme oldTheme = oldThemeInfo.Item1;
                if (oldTheme != null && oldTheme != newTheme)
                {
                    ResourceDictionary oldThemeResource =
                        resources.MergedDictionaries.FirstOrDefault(d => d.Source == oldTheme.Resources.Source);
                    if (oldThemeResource != null)
                    {
                        resources.MergedDictionaries.Add(newTheme.Resources);
                        resources.MergedDictionaries.Remove(oldThemeResource);

                        themeChanged = true;
                    }
                }
            }
            else
            {
                ChangeAppStyle(resources, newAccent, newTheme);
                themeChanged = true;
            }

            if (themeChanged)
            {
                OnThemeChanged(newAccent, newTheme);
            }
        }

        [SecurityCritical]
        private static void ApplyResourceDictionary(ResourceDictionary newRd, ResourceDictionary oldRd)
        {
            oldRd.BeginInit();

            foreach (DictionaryEntry entry in newRd)
            {
                if (oldRd.Contains(entry.Key))
                {
                    oldRd.Remove(entry.Key);
                }

                oldRd.Add(entry.Key, entry.Value);
            }

            oldRd.EndInit();
        }

        /// <summary>
        ///     Scans a resources and returns it's accent and theme.
        /// </summary>
        /// <param name="resources">The ResourceDictionary to check.</param>
        private static Tuple<AppTheme, Accent> DetectAppStyle(ResourceDictionary resources)
        {
            AppTheme currentTheme = null;
            Tuple<AppTheme, Accent> detectedAccentTheme = null;

            if (TryDetectThemeFromResources(ref currentTheme, resources))
            {
                if (TryGetThemeFromResources(currentTheme, resources, ref detectedAccentTheme))
                {
                    return new Tuple<AppTheme, Accent>(detectedAccentTheme.Item1, detectedAccentTheme.Item2);
                }
            }

            return null;
        }

        private static bool TryDetectThemeFromResources(ref AppTheme detectedTheme, ResourceDictionary dict)
        {
            using (IEnumerator<ResourceDictionary> enumerator = dict.MergedDictionaries.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    ResourceDictionary currentRd = enumerator.Current;

                    AppTheme matched;
                    if ((matched = GetAppTheme(currentRd)) != null)
                    {
                        detectedTheme = matched;
                        enumerator.Dispose();
                        return true;
                    }

                    if (TryDetectThemeFromResources(ref detectedTheme, currentRd))
                    {
                        return true;
                    }
                }
                return false;
            }
        }

        /// <summary>
        ///     Invalidates global colors and resources.
        ///     Sometimes the ContextMenu is not changing the colors, so this will fix it.
        /// </summary>
        [SecurityCritical]
        private static void OnThemeChanged(Accent newAccent, AppTheme newTheme)
        {
            SafeRaise.Raise(IsThemeChanged, Application.Current, new ThemeChangedEventArgs(newTheme, newAccent));
        }

        #endregion

        /// <summary>
        ///     This event fires if accent color and theme was changed
        ///     this should be using the weak event pattern, but for now it's enough
        /// </summary>
        public static event EventHandler<ThemeChangedEventArgs> IsThemeChanged;
    }
}